"use client"; /** * /w/:workspace/trash — trashed pages (delete sets `trashedAt`; FR-PR-028). * * List → GET /workspaces/:wid/pages?trashed=true ({ data: Page[] } of the * trashed ROOTS — a trashed subtree appears once, at its root; * server-gated to Admin/Owner per docs/api/pages.md §3) * Restore → POST /pages/:id/restore (recursive, EDIT) * Purge → DELETE /pages/:id/permanent (hard delete — owner/admin * or the owner of their own page) * * The list is ordered by `trashedAt` (newest first, client-side — the API * orders by position so its cursor stays consistent). A trashed DATABASE page * shows the database icon (its `databaseId` is non-null — the Page model has * no `type` field; the Database row is the marker). */ import { useMemo } from "react"; import { useParams } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Database as DatabaseIcon, FileText, Loader2, RotateCcw, Trash2 } from "lucide-react"; import { formatRelativeTime } from "@/lib/utils"; import { apiFetch, pageApi } from "@/lib/api"; import { useWorkspace } from "@/hooks/use-workspace"; import { Page } from "@/lib/types"; export default function TrashPage() { const params = useParams<{ workspace: string }>(); const slugOrId = params?.workspace ?? ""; const { data: workspace } = useWorkspace(slugOrId); const qc = useQueryClient(); const trashed = useQuery({ queryKey: ["workspaces", workspace?.id ?? slugOrId, "trash"], queryFn: () => pageApi.list(workspace?.id ?? slugOrId, { trashed: true, limit: 200 }), enabled: !!workspace?.id, retry: false }); // Both restore and purge must also refresh the page tree: a restored page // re-appears under its parent, and a purged page must be gone. const refresh = () => { qc.invalidateQueries({ queryKey: ["workspaces", workspace?.id, "trash"] }); qc.invalidateQueries({ queryKey: ["workspaces", workspace?.id, "page-tree"] }); }; const restore = useMutation({ mutationFn: (pageId: string) => pageApi.restore(pageId), onSuccess: refresh }); const purge = useMutation({ mutationFn: async (pageId: string): Promise => { await apiFetch(`/pages/${pageId}/permanent`, { method: "DELETE" }); }, onSuccess: refresh }); // Purge is owner/admin-only server-side. Compared case-insensitively because // the API returns the raw UPPERCASE role enum while core types are lowercase // (flagged to the core agent). const role = (workspace?.role ?? "").toLowerCase(); const canPurge = role === "owner" || role === "admin"; // The API orders the trash by position (cursor consistency); show newest // first (the `trashedAt` recency the page's copy promises). const items = useMemo( () => [...(trashed.data ?? [])].sort( (a, b) => new Date(b.trashedAt ?? 0).getTime() - new Date(a.trashedAt ?? 0).getTime() ), [trashed.data] ); return (

Trash

Deleted pages are kept here. Restore to bring them back, or purge to delete permanently.

{trashed.isLoading && (

Loading trash…

)} {trashed.isError && (

Couldn't load the trash. Viewing the trash requires the Admin or Owner role.

)} {!trashed.isLoading && !trashed.isError && items.length === 0 && (

The trash is empty.

)} {/* `trash-section` (tests/README.md) is the SIDEBAR Trash node (the navigation target) — it lives on the sidebar, not this list, so a single element owns it (strict-mode safe). */}
); }